package com.lnsoft.dqfs.server.handle.codec;

import com.lnsoft.dqfs.server.dao.DqfsDao;
import com.lnsoft.dqfs.server.util.Arrays16;
import com.lnsoft.dqfs.server.util.MessageUtil;
import com.lnsoft.dqfs.server.util.UpgradePackageCache;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.ReplayingDecoder;

import java.math.BigDecimal;
import java.nio.ByteBuffer;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.List;
import java.util.Map;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 101协议平衡链路解析
 *                 //  当前的流程：
 *                 //          1.终端发起连接认证(固定帧：frameType：199),主站确认(固定帧：frameType：0)
 *                 //          2.主站请求链路状态(固定帧：frameType：73),终端确认(固定帧：frameType：139)
 *                 //          3.主站请求复位链路(固定帧：frameType：64),终端确认(固定帧：frameType：128)
 *                 //          -------初始化结束------
 *                 //          4.终端上报初始化结束(变长帧:ti:70),主站确认(固定帧:frameType：0)
 *                 //          5.主站请求时钟同步(变长帧:ti:103),终端确认(固定帧：frameType：128)
 *                 //          6.终端请求时钟同步(变长帧:ti:103),主站确认(固定帧:frameType：0)
 *                 //          7.终端直接发送文件(变长帧:ti:211),主站确认(固定帧:frameType：0)(注:主动上传采用文件传输格式,主站不用读激活过程,终端直接上传文件内容)
 *                 //          8.主站请求终端参数(变长帧:ti:202),终端确认(固定帧：frameType：128)
 *                 //          9.终端请求上报参数(变长帧:ti:202),主站确认(固定帧:frameType：0)
 *                 //          10.主站请求下发写文件激活(变长帧:ti:210),终端确认(固定帧：frameType：128)
 *                 //          11.终端请求下发写文件确认激活(变长帧:ti:210),主站确认(固定帧：frameType：0)
 *                 //          12.主站请求下发写文件(变长帧:ti:210),终端确认(固定帧：frameType：128)
 *                 //          ...(下发文件报文)
 *                 //          ...(下发文件报文)
 *                 //          ...(下发文件报文)
 *                 //          13.终端请求下发写文件完毕(变长帧:ti:210),主站确认(固定帧：frameType：0)
 *                 //          14.主站请求下发复位进程指令(变长帧:ti:210),终端确认(固定帧：frameType：128)
 */
//@Slf4j
public class Dqfs101Decoder extends ReplayingDecoder<Void> {

    //private static final CacheClient cache = CacheClient.getInstance();
	private static final Logger log = LoggerFactory.getLogger(Dqfs101Decoder.class);

    private Frame frame;

    private UpgradePackageCache upgradePackageCache;

    private DqfsDao dqfsDao;

    public Dqfs101Decoder(Frame frame, UpgradePackageCache upgradePackageCache, DqfsDao dqfsDao) {
        this.frame = frame;
        this.upgradePackageCache = upgradePackageCache;
        this.dqfsDao = dqfsDao;
    }

    @Override
    protected void decode(ChannelHandlerContext ctx, ByteBuf byteBuf, List<Object> out) throws Exception {

        try {
            frame.setReadTimeOutNumber(0);

            int clientName = frame.getClientName();
            int port = frame.getServerPort();

            int headFrame = (byteBuf.readByte() & 0xFF);
            if (headFrame == 16) { // 固定长度帧 6个字节
                byte[] bytes = new byte[6];
                bytes[0] = (byte) headFrame;
                byteBuf.readBytes(bytes, 1, 5);

                if (clientName == 0) {
                    ByteBuf buffer = Unpooled.wrappedBuffer(new byte[]{bytes[2], bytes[3]});
                    try {
                        clientName = buffer.getUnsignedShortLE(0);
                        frame.setClientName(clientName);
                    } finally {
                        buffer.release();
                    }
                }

                frame.setA1(bytes[2]);
                frame.setA2(bytes[3]);
                log.debug("客户端{}:{},终端固定帧报文:{}", clientName, port, Arrays16.toString(bytes));
                int frameType = (bytes[1] & 0xFF);

                //  终端刚开始接入时：ClientId（数据库id）为空,因为此时还没有检查终端身份;并且报文类型应该为0xC7(199)终端注册报文
                if ((frame.getClientId() == null && frameType != 199) ||
                        MessageUtil.createCS(bytes, 1, 4) != (bytes[4] & 0xFF)) {
                    log.warn("客户端{}:{},报文校验和异常，或者客户端试图跳过身份认证;异常报文：{}",clientName, port, Arrays16.toString(bytes));

                    //  构造确认报文
                    bytes[1] = 1;
                    bytes[4] = (byte) MessageUtil.createCS(bytes, 1, 4);
                    out.add(new RespFrame(bytes,null));
                    return;
                }
                if (frameType == 199) { // 收到终端身份验证报文(16进制C7)(终端主发)
                    //  构造服务帧：后面线程池中检查终端身份
                    out.add(new RespFrame(null,new ConnectServiceFrame(bytes)));
                    return;
                }
                if (frameType == 139) { // 收到终端回复的链路状态确认报文(16进制8B)(终端确认)
                    //  重置主站各种状态
                    frame.setProcess(0);
                    frame.setUpServerFcb(false);
                    //  反转主站的反转位FCB
                    frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                    frame.setSendingFrame(null);
                    //  构造请求帧:请求‘复位远方链路’
                    bytes[1] = 64;
                    bytes[4] = (byte) MessageUtil.createCS(bytes, 1, 4);
                    out.add(new RespFrame(null,new ResetLinkServiceFrame(bytes)));
                    return;
                }



                //  该类型报文可能是以下几种情况：
                //  1.终端收到复位链路指令的确认报文
                //  2.终端收到时钟同步指令的确认报文
                //  3.终端收到复位进程指令的确认报文
                //  4.终端收到主站请求参数指令的确认报文
                //  5.终端收到主站下发文件激活的确认报文
                //  6.终端收到主站下发文件数据的确认报文
                if (frameType == 128) { //  (16进制80)(终端确认)

                    switch (frame.getProcess()) {
                        //  复位链路
                        case 64:
                            log.info("客户端{}:{},终端收到‘复位链路指令’，重置反转位FCB和流程状态信息" ,clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //  时钟同步
                        case 103:
                            log.info("客户端{}:{},终端确认报文<时钟同步指令>",clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //  复位进程
                        case 105:
                            log.info("客户端{}:{},终端确认报文<复位进程指令>",clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //  终端确认收到读参数指令
                        case 202:
                            log.info("客户端{}:{},终端确认报文<请求参数指令>", clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //  终端确认收到 参数预置 指令
                        case 203:
                            log.info("客户端{}:{},终端确认报文<参数预置指令>", clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //  终端确认收到 参数固化 指令
                        case 2030:
                            log.info("客户端{}:{},终端确认报文<参数固化指令>", clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        //	终端确认收到文件激活指令
                        case 210:
                            log.info("客户端{}:{},终端确认报文<下发写文件激活>", clientName, port);
                            frame.setUpServerFcb(false);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setProcess(0);
                            frame.setSendingFrame(null);
                            break;
                        case 2100:
                            log.info("客户端{}:{},终端确认报文<下发写文件数据>", clientName, port);
                            frame.setServerFcb((byte) (frame.getServerFcb() ^ 1));
                            frame.setSendingFrame(null);
                            break;
                        default:
                            log.info("客户端{}:{}, 可能收到终端的测试链路的确认报文", clientName, port);
                    }

                    // 判断是否是文件写入传输过程中
                    if (frame.getUpgradePackageindex() > 0) {
                        //  构造状态‘写文件过程中’消息
                        out.add(new RespFrame(null,new UpgradePackageServiceFrame(1)));
                        return;
                    }

                    out.add(new RespFrame(null,null));
                    return;
                }
                log.warn("客户端{}:{},未定义格式的报文{}", clientName, port, Arrays16.toString(bytes));
                out.add(new RespFrame(null,null));

            } else if (headFrame == 104) {      //  变长帧
                int length = (byteBuf.readByte() & 0xFF);
                byte[] bytes = new byte[length + 6];
                bytes[0] = (byte) headFrame;
                bytes[1] = (byte) length;
                byteBuf.readBytes(bytes, 2, length + 4);
                byte c = bytes[4];
                byte fcv = (byte) (c >> 4 & 0x1);
                byte fcb = (byte) (c >> 5 & 0x1);
                int ti = bytes[7] & 0xFF; // 类型标识

                log.debug("客户端{}:{},终端请求变长报文,ti类型:<{}>,报文详情:{}", clientName, port, ti, Arrays16.toString(bytes));

                // int cot = bytes[9] & 0xFF;

                if ( (ti != 70 && !frame.isConnectFlag()) ||
                        MessageUtil.createCS(bytes, 4, bytes.length - 2) != (bytes[bytes.length - 2] & 0xFF)) {
                	log.error("RealyCS:{}   >>>>   CS:{}", MessageUtil.createCS(bytes, 4, bytes.length - 2), bytes[bytes.length - 2] & 0xFF);
                    log.error("客户端{}:{}报文异常：fcb不匹配，或者没有发送初始化完毕报文，或者cs校验和验证不通过；异常报文：{}", clientName, port, Arrays16.toString(bytes));
                    // fcb 错误
                    out.add(new RespFrame(MessageUtil.createUnConfirmFrame(bytes[5], bytes[6]),null));
                    ctx.channel().close();
                    return;
                }
                //  在终端上传文件数据流程中，如果控制反转位没有反转,则认为该报文为重复报文
                //  主站仅回复确认报文，不反转FCB控制反转位、上传的数据舍弃不使用；
                if (fcv == 1 && fcb != frame.getClientFcb() && ti == 211) {
                    log.debug("客户端{}:{},终端在数据上传流程中，可能发送了重复报文;终端的FCB没有反转,FCB值应当为:{},而报文中实际FCB值为:{}", clientName, port, frame.getClientFcb(),fcb);

                    //  丢弃报文数据,仅回复确认报文
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));

                    return;
                }
                if (fcv == 1) {
                    frame.setClientFcb((byte) (frame.getClientFcb() ^ 1));
                    log.debug("客户端{}:{},终端的clientFcb被反转；当前值:{}", clientName, port, frame.getClientFcb());
                }

//                log.debug("客户端{}:{},终端请求报文,ti类型:{}",clientName, port, ti);

                if (ti == 70) { // 收到终端的初始化结束报文(终端主发)
                    // 加入缓存 目前不需要加缓存  上传数据结束后 看看是否有待完成的任务 如 升级 修改配置信息
                    // cache.cache(frame.getClientId(),ctx.channel());
                    frame.setConnectFlag(true);


                    //  构造‘初始化结束确认报文’，并且通知线程池发送‘请求时钟同步报文’
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),new TimeSyncServiceFrame()));
                    log.debug("客户端{}:{},终端请求报文<初始化结束>", clientName, port);
                    return;
                }
                if (ti == 30 || ti == 36 || ti == 103) { // 30:SOE上传(突发事件)(终端主发)  36:变化遥测 103:时间同步(终端主发)
                    //  构造确认报文
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                    String info = null;
                    switch (ti) {
                        case 30:
                            info = "SOE上传(突发事件)";
                            break;
                        case 36:
                            info = "变化遥测报文";
                            break;
                        case 103:
                            info = "时间同步(终端主发)";
                    }

                    log.debug("客户端{}:{},终端请求报文<{}>", clientName, port, info);
                    return;
                }

                if (ti == 211) { // 主动上传文件
                    int isEnd = bytes[24] & 0xFF; // 0:本次传输完成
                    // 文件字节流 25~(len-3)
                    frame.getByteBuf().writeBytes(bytes, 25, bytes.length - 28);
                    if (isEnd == 0) { // 本次文件传输完成
                        //  构造文件上传完成的确认报文
                        out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                        //  创建文件上传完成的服务消息;线程池收到此消息时：文件传输完毕，是否有升级（查看版本号）及修改参数的操作；
                        out.add(new RespFrame(null,new UploadServiceFrame(false)));

                        //  标识终端数据上传完毕
//                        frame.setFinished(true);
                        log.info("客户端{}:{},终端请求报文<终端数据上传完毕>",clientName,port);

                        return;
                    }
                    //  构造确认报文
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                    return;
                }
                //  一般情况下，主站发送ti=202的读参报文，终端回复确认报文；然后，终端将具体参数报文发送到主站，主站回复确认报文.
                if (ti == 202) {    //  收到终端主发的参数报文
                    byte argLength = bytes[18];
                    ByteBuffer byteBuffer = ByteBuffer.allocate(argLength).put(bytes, 19, argLength);
                    byteBuffer.flip();
                    String argVal = MessageUtil.asciiStr(byteBuffer, argLength);
                    log.debug("客户端{}:{},终端请求报文<版本号参数>:{}", clientName, port, argVal);

                    //  构造确认报文
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));

                    Map<String, Object> version = dqfsDao.getVersion(frame.getClientId());
                    boolean flag = false;
                    if (version != null && version.containsKey("SJBBH")) {
                        if (version.get("SJBBH") != null && !((String)version.get("SJBBH")).equals(argVal)  && ((BigDecimal) (version.get("SJJG"))).intValue() == 0) {
                            flag = true;
                        }

                        log.info("客户端{}:{},数据库中升级记录对应的版本信息:{},对应的升级状态:{}",clientName,port,version,version.get("SJJG"));

                        log.debug("客户端{}:{},读参报文,解析参数(版本号):{}", clientName, port, argVal);
                    }

                    if (upgradePackageCache.isUpgradePackage(argVal) && flag) {
                        log.debug("客户端{}:{},触发升级操作，写文件激活信号", clientName, port);
                        //  记录终端升级前的版本号
                        frame.setOldVersion(argVal);
//                        //  记录数据库的对应的升级记录
//                        //  TODO 和天洋确认升级条件：最近一次待升级记录,不论时间是否是过去的时间还是将来的时间
                        frame.setRecordId((String) version.get("ID"));
                        // 版本号不一致 触发升级操作 写文件激活
                        out.add(new RespFrame(null,new UpgradePackageServiceFrame(0)));
                    } else {

                        log.debug("客户端{}:{},触发参数修改", clientName, port);

                        // 触发参数修改 目前参数方面需求是 实现主(子)站ip 端口修改
                        out.add(new RespFrame(null,new UpConnectArgServiceFrame()));
                    }
                    return;
                }

                //  文件服务报文帧(主要用来升级终端版本文件)
                if (ti == 210) {
                    // 终端主发 写文件确认激活 的请求报文
                    //  （注意事项）升级终端版本文件过程中：终端对‘文件流’使用‘固定帧’短报文进行确认；
                    // 操作标识
                    int operationIdentifer = bytes[15] & 0xFF;
                    // 结果描述
                    int result = bytes[16] & 0xFF;
                    if (operationIdentifer == 8 && result == 0) {
                        log.info("客户端{}:{},终端请求报文<写文件确认激活>", clientName, port);
                        //  构造激活确认报文
                        out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                        frame.setUpgradePackageindex(0);

                        //  构造‘可以开始写文件’的消息,指示线程池写文件给终端
                        out.add(new RespFrame(null,new UpgradePackageServiceFrame(1)));
                        return;
                    } else if (operationIdentifer == 10) {
                        //  如果result=0,则认为：终端主动上报‘写文件完成’(需要进行报文确认)；
                        result = bytes[24] & 0xFF;
                        if (result == 0) {
                            log.debug("客户端{}:{},终端请求报文:<下发写文件数据完毕>", clientName, port);
                            //  构造确认报文
                            out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));

                            //  此处根据终端通知的文件传输完毕来确认主站发送完成
                            frame.setUpServerFcb(false);
                            frame.setProcess(0);
                            frame.setSendingFrame(null);

                            // 写文件传输完成后 发送重启报文
                            out.add(new RespFrame(null,new ResetServiceFrame()));

                            //  通知线程池:升级结果
                            //  此时断定对终端版本升级下发文件完成（因为终端上报了‘下发写文件数据完毕’）,此时记录升级结果到数据库

                            log.info("客户端{}:{},主站将升级结果信息写入数据库",clientName,port);
                            log.info("客户端{}:{},升级前的版本号:{},升级记录数据库ID:{}",clientName,port,frame.getOldVersion(),frame.getRecordId());
//
                            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                            String date = sdf.format(new Date());
                            dqfsDao.updateIacmUpgradeResult(frame.getRecordId(),date,1,frame.getOldVersion(),"升级成功");

                            //  升级成功之后会将升级指令recordId置为null
                            frame.setOldVersion(null);
                            frame.setRecordId(null);

                            /**
                             * 断定升级失败的标志有两个：
                             * 1.process = 210 || process = 2100
                             * 2.recordId != null
                             *
                             * 所以，当发生异常时检查recordId或者process标志
                             * */

                            return;
                        }
                    }
                    log.warn("客户端{}:{},终端请求报文，未知类型@@@@@", clientName, port);
//                    log.warn("客户端{}:{}ti:{},文件传输,操作标识：{},结果描述:{}", clientName, port, ti, operationIdentifer, result);

                }

                //  一般情况下，首先主站发送‘请求进程复位指令’给终端，终端回复一般的确认报文（表示指令收到）；终端执行复位进程任务，待复位完成，将发送
                //  ‘进程复位完成的报文’给主站，主站回复确认报文。
                if (ti == 105) {    //  终端复位进程完成报文(终端主发)w
                    // 构造确认报文
                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                    return;
                }

                if (ti == 203) {
                    //68 29 29 68 d3 2 10 cb 5 7 2 10 0 0 80 20 82 21 2 c0 0 21 82 21 2 a8 0 22 82 21 2 a 0 23 82 21 2 14 0 24 82 21 2 c6 f 8c 16
                    // 参数预置确认 + 参数固化确认
                    byte se = bytes[14];
                    if ((se >> 7 & 0x1) == 1) {
                        //参数预置确认
                        out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                        // 固话命令下发
                        out.add(new RespFrame(null,new ArgSolidifyServiceFrame()));
                        return;
                    }


                    out.add(new RespFrame(MessageUtil.createConfirmFrame(bytes[5], bytes[6]),null));
                    // 参数固化成功

                    // IP PORT 修改
                    if (frame.getArgId() != null) {
                        dqfsDao.updateConnectArg(frame.getArgId());
                        frame.setArgId(null);
                    }
                    // 参数固化成功后 发起复位进程(相当于重启)指令
                    out.add(new RespFrame(null,new ResetServiceFrame()));

                    return;
                }

                out.add(new RespFrame(null,null));
                return;

            } else {
                log.info("客户端{}:{},含有脏数据!!! >>> :{}", frame.getClientName(), port, Integer.toHexString(headFrame));
                out.add(new RespFrame(null,null));
            }
        } catch (Exception e) {
            log.error("客户端{}:{},decoder error!!!{}", frame.getClientName(), frame.getServerPort(), e.toString());


            if (frame.getRecordId() != null) {
                log.info("客户端{}:{},升级失败，更新升级结果到数据库", frame.getClientName(), frame.getServerPort());
                SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
                String date = sdf.format(new Date());
                dqfsDao.updateIacmUpgradeResult(frame.getRecordId(),date,2,frame.getOldVersion(),e.toString());
            }

            throw e;
        }


    }

}

/**
 *
 * //////////////////////////2019-10-28
 *
 *	沟通之后确认的重传机制
 *
 * 1、主站主发的请求报文30s未收到对应确认报文，则重发该请求报文，如果重发3遍该报文都未收到确认报文，则断开连接；
 *
 * 2、主站收到 终端主发的FCB控制反转位没有正常反转的 报文，则只对该报文发送确认报文，报文数据舍弃不做处理；
 *
 * 3、主站不关注主站向终端发送的确认报文是否被收到；主站只对终端的请求报文做确认回复；
 *
 * 4、一般情况下，主站侧，不会重置链路的反转位FCB；也就是说，主站所有下发的请求报文都会连续反转FCB反转位；
 *
 *
 * 5. 终端复位进程完成报文
 *
 *
 * 6.对终端修改参数成功之后，会下发固化报文和重启报文；而升级完成之后不下发固化报文和重启报文；
 * 7.主站下发的升级数据报文，会检查发送的报文是否是最后一个‘升级报文’；
 *
 * 8.终端上传完数据之后，主站会下发 请求参数报文，根据返回的参数报文，检查是否需要升级，如果不需要升级版本，则进入参数修改流程，修改参数之前会检查该终端是否需要修改参数;
 *
 * */
